package com.aop;

import com.annotation.RequestLimit;
import com.core.websocket.WebSocket;
import com.exception.RequestLimitException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: lxf
 * Date: 2016/10/31
 * Time: 15:20
 * To change this template use Editor | File and Code Templates.
 */
@Aspect
public class RequestLimitAop {

    private static final Logger logger = LoggerFactory.getLogger(RequestLimitAop.class);
    @Autowired
    private RedisTemplate redisTemplate;
    private static Map<String, LinkedList<Long>> requestLimitMap = new ConcurrentHashMap<>();

    @Before("within(@org.springframework.stereotype.Controller *) && @annotation(limit)")
    public void requestLimit(JoinPoint joinPoint, RequestLimit limit) throws RequestLimitException {
        try {
            Object[] args = joinPoint.getArgs();
            HttpServletRequest request = null;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof HttpServletRequest) {
                    request = (HttpServletRequest) args[i];
                    break;
                }
            }
            if (request == null) {
                throw new RequestLimitException("方法中缺失HttpServletRequest参数");
            }
            String ip = WebSocket.getIpAddress(request);
            String url = request.getRequestURL().toString();
            String key = "req_limit_".concat(url).concat("_").concat(ip);
            boolean checkResult = checkWithHashMap(limit, key);
            /*boolean checkResult = checkWithRedis(limit, key);*/
            if (!checkResult) {
                logger.debug("requestLimited," + "[用户IP:{}],[访问地址:{}]超过了限定的次数[{}]次", ip, url, limit.count());
                throw new RequestLimitException();
            }
        } catch (RequestLimitException e) {
            throw e;
        } catch (Exception e) {
            logger.error("RequestLimitAop.requestLimit has some exceptions: ", e);
        }
    }

    /**
     * 以HashMap实现请求记录
     *
     * @param limit 限制的次数
     * @param key   缓存key值
     * @return 是否超出限制
     */
    private boolean checkWithHashMap(RequestLimit limit, String key) {
        LinkedList<Long> requestTimes = requestLimitMap.get(key);
        long currentTime = System.currentTimeMillis();
        if (requestTimes == null || requestTimes.isEmpty()) {
            requestTimes = new LinkedList<>();
        }
        requestTimes.addLast(currentTime);
        requestLimitMap.put(key, requestTimes);
        long firstRequestTime = requestTimes.getFirst();
        requestTimes = removeOverTime(limit.time(), currentTime, requestTimes);
        //本次次请求和最早一次请求时间间隔 < 设定时间、且请求次数超过设定次数 则判定为禁止请求 ，单位：毫秒
        return (currentTime - firstRequestTime) >= limit.time() && requestTimes.size() <= limit.count();
    }

    /**
     * 以redis实现请求记录
     *
     * @param limit 限制的次数
     * @param key   缓存key值
     * @return 是否超出限制
     */
    private boolean checkWithRedis(RequestLimit limit, String key) {
        long count = redisTemplate.opsForValue().increment(key, 1);
        if (count == 1) {
            redisTemplate.expire(key, limit.time(), TimeUnit.MILLISECONDS);
        }
        return count <= limit.count();
    }

    /**
     * 若本次请求和最早一次请求时间间隔 > 设定时间 则递归移除最早一次请求时间
     *
     * @param limitTime    限制请求的时间
     * @param currentTime  当前时间
     * @param requestTimes 请求时间
     * @return 移除无用的时间后的请求时间
     */
    private LinkedList<Long> removeOverTime(long limitTime, long currentTime, LinkedList<Long> requestTimes) {
        if ((currentTime - requestTimes.getFirst()) > limitTime) {
            requestTimes.removeFirst();
            removeOverTime(limitTime, currentTime, requestTimes);
        }
        return requestTimes;
    }


}
